﻿namespace Contoso.ActiveDirectory {

    #region Usings
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.DirectoryServices;
    using System.DirectoryServices.ActiveDirectory;
    using System.Globalization;
    using System.IO;
    using System.Linq;
    using System.Reflection;
    using System.Runtime.Serialization.Formatters.Binary;
    using System.Security;
    using System.Security.AccessControl;
    using System.Security.Principal;
    using System.Text.RegularExpressions;
    using System.Threading;
    using System.Xml.Linq;
    using System.Xml.Serialization;
    #endregion

    /// <summary>
    /// Base class for attributes that are common to all AD objects
    /// </summary>
    [Serializable]
    public class ADBaseObjectProperties : IADBaseObjectProperties {

        #region Members

        /// <summary>
        /// Indicates that a given object has had its ACLs changed to a more secure value by the system because it was a member of 
        /// one of the administrative groups (directly or transitively).
        /// </summary>
        public int AdminCount { get; private set; }

        /// <summary>
        /// The constructed name in the form of: contoso.com/OU/OU/CN
        /// </summary>
        public string CanonicalName { get; protected set; }

        /// <summary>
        /// The LDAP common name
        /// </summary>
        public string CN {
            get {
                if (string.IsNullOrWhiteSpace(cn)) {
                    cn = ADDataAccess.GetCNFromDN(this.DN);
                }
                return cn;
            }
            private set {
                cn = value;
            }
        }
        private string cn;

        /// <summary>
        /// http://technet.microsoft.com/en-us/library/ee198815.aspx
        /// Even though the description attribute is multivalued, it behaves like a single-valued attribute 
        /// when performing append or delete operations because it can contain a maximum of one entry. 
        /// This is to allow for pre-Active Directory compatibility, and this attribute behaves as a 
        /// single-valued attribute only for security principals such as the user account object.
        /// </summary>
        /// <remarks>Has control characters removed</remarks>
        public string Description {
            get { return description; }
            private set { description = value.RemoveControlCharacters(); }
        }
        private string description;

        /// <summary>
        /// Temporary collection used for building this object
        /// </summary>
        protected Dictionary<string, object[]> Attributes { get; set; }

        /// <summary>
        /// AttributesToGet is most useful in higher-level objects, to limit the amount of attributes returned in a query
        /// to those relevant only for that type.
        /// Not as useful for User or Computer objects.
        /// </summary>
        /// <remarks>Used by external classes</remarks>
        public static readonly string[] AttributesToGet;

        /// <summary>
        /// Specifies that the object used to create this object is destroyed (set to null) after it completes.
        /// </summary>
        public bool DestroyInputObject { get; protected set; }

        /// <summary>
        /// The distinguished name of this object
        /// </summary>
        public string DN { get; protected set; }

        /// <summary>
        /// The domain DNS name
        /// </summary>
        public string DNSDomain { get; protected set; }

        /// <summary>
        /// The Domain short name that this object is part of
        /// </summary>
        public string Domain { get; protected set; }

        /// <summary>
        /// The domain CN part of the fully-qualified domain name that this object is part of
        /// </summary>
        public string DomainCN {
            get {
                domainCN = string.Empty;
                if (!string.IsNullOrWhiteSpace(this.DomainDN)) {
                    domainCN = this.DomainDN.Substring(this.DomainDN.IndexOf("DC=", StringComparison.OrdinalIgnoreCase));
                    domainCN = domainCN.Substring("DC=".Length, domainCN.IndexOf(",", StringComparison.OrdinalIgnoreCase) - "DC=".Length).ToUpper();
                }
                return domainCN;
            }
        }
        private string domainCN;

        /// <summary>
        /// The distinguished name of the domain that this object is part of
        /// </summary>
        public string DomainDN { get { return ADDataAccess.GetDomainDNFromDN(this.DN); } }

        /// <summary>
        /// The guid of the NTDS Settings object of the domain controller that last modified the object
        /// </summary>
        public Guid DSASignature { get; protected set; }

        public CustomAttributes CustomAttributes { get; protected set; }

        /// <summary>
        /// Flag used to determine if an object is valid by testing properties to confirm that they have the expected values.
        /// </summary>
        public bool IsValid { get; protected set; }

        /// <summary>
        /// The formal name of the Info attribute is CN=Comment
        /// Info = Notes field in AD Users and Computers.
        /// </summary>
        /// <remarks>Has control characters removed</remarks>
        public string Info {
            get { return info; }
            private set { info = value.RemoveControlCharacters(); }
        }
        private string info;

        public bool IsCriticalSystemObject { get; protected set; }

        /// <summary>
        /// Describes if an object is deleted but restorable
        /// </summary>
        public bool IsDeleted { get; protected set; }

        /// <summary>
        /// Determines if an object has the Deny ACE for Everyone for Delete
        /// </summary>
        public bool IsProtectedFromDelete {
            get {
                return ADDataAccess.IsProtectedFromDelete(this.AccessRules);
            }
        }

        /// <summary>
        /// Describes if an object is deleted but not restorable
        /// </summary>
        public bool IsRecycled { get; protected set; }

        /// <summary>
        /// The Distinguished Name (DN) of the last known parent of an orphaned object.
        /// </summary>
        public string LastKnownParent { get; protected set; }

        public string LdapDN { get; protected set; }

        /// <summary>
        /// The DateTime the object was modified.  Constructed.
        /// </summary>
        public DateTime ModifyTimeStampUTC { get; private set; }

        /// <summary>
        /// This attribute holds the original relative distinguished name (RDN) of a deleted object.
        /// </summary>
        public string MSDSLastKnownRDN { get; protected set; }

        public string Name { get; protected set; }

        public string ObjectCategory { get; protected set; }

        public IReadOnlyList<string> ObjectClass { get; protected set; }

        public Guid ObjectGuid { get; protected set; }

        /// <summary>
        /// Warning: Do not assume that the presence of an ObjectSid for a group means that the group is SecurityEnabled!
        /// Non-security-enabled groups have this value set.  To determine if a group is security-enabled, use the
        /// GroupType property.
        /// </summary>
        public SecurityIdentifier ObjectSid {
            get { return objectSid; }
            protected set { objectSid = value; }
        }
        [NonSerialized]
        private SecurityIdentifier objectSid;

        /// <summary>
        /// The distinguished name of the parent container of the object
        /// </summary>
        public string ParentDN { get; protected set; }

        /// <summary>
        /// The server name that is embedded in the bind path, if available.  
        /// This is the server that the object was returned from.
        /// </summary>
        public string Server { get; protected set; }

        /// <summary>
        /// http://msdn.microsoft.com/en-us/library/cc220886%28v=prot.10%29.aspx
        /// </summary>
        /// <remarks>Object is not visible unless the Advanced menu option is selected in AD Users and Computers</remarks>
        public bool ShowInAdvancedViewOnly { get; protected set; }

        /// <summary>
        /// http://msdn.microsoft.com/en-us/library/cc220919%28v=prot.10%29.aspx
        /// http://msdn.microsoft.com/en-us/library/cc223141%28v=prot.10%29.aspx
        /// </summary>
        /// <remarks>
        /// Flags enum
        /// Value            Description
        /// 1 (0x00000001)  NR FLAG_ATTR_NOT_REPLICATED or FLAG_CR_NTDS_NC.  
        ///                 When used on an attributeSchema object, it specifies that this attribute is not replicated. 
        ///                 If it is used on a crossRef object, it specifies that the NC that the crossRef is for is an Active Directory NC.
        ///                 
        /// 2 (0x00000002)  PS FLAG_ATTR_REQ_PARTIAL_SET_MEMBER or FLAG_CR_NTDS_DOMAIN.  
        ///                 When used on an attributeSchema object, it specifies that the attribute is a member of a partial 
        ///                 attribute set (PAS). If used on a crossRef object, it specifies that the NC is a domain NC.
        ///                 
        /// 4 (0x00000004)  CS FLAG_ATTR_IS_CONSTRUCTED or FLAG_CR_NTDS_NOT_GC_REPLICATED
        ///                 When used on an attributeSchema object, this flag specifies that the attribute 
        ///                 is a constructed attribute. If used on a crossRef object, it specifies that the NC is not to be 
        ///                 replicated to GCs.
        ///                 
        /// 8 (0x00000008)  OP FLAG_ATTR_IS_OPERATIONAL
        ///                 Only used on an attributeSchema object. It specifies that the attribute is an operational attribute.
        ///                 
        /// 16 (0x00000010) BS FLAG_SCHEMA_BASE_OBJECT
        ///                 Only used on attributeSchema and classSchema objects. It specifies that this attribute or class 
        ///                 is part of the base schema.  Modifications to base schema objects are specially restricted.
        ///                 
        /// 32 (0x00000020) RD FLAG_ATTR_IS_RDN
        ///                 Only used on an attributeSchema object. It specifies that this attribute can be used as an RDN attribute.
        /// 
        /// 33554432        DE FLAG_DISALLOW_MOVE_ON_DELETE
        /// (0x02000000)    Specifies that the object does not move to the deleted-objects container when the object is deleted. 
        /// 
        /// 67108864        DM FLAG_DOMAIN_DISALLOW_MOVE
        /// (0x04000000)    Specifies that if the object is in a domain NC, the object cannot be moved.
        /// 
        /// 134217728       DR FLAG_DOMAIN_DISALLOW_RENAME
        /// (0x08000000)    Specifies that if the object is in a domain NC, the object cannot be renamed. 
        /// 
        /// 268435456       AL FLAG_CONFIG_ALLOW_LIMITED_MOVE
        /// (0x10000000)    Specifies that if the object is in the config NC, the object can be moved, with restrictions. 
        /// 
        /// 536870912       AM FLAG_CONFIG_ALLOW_MOVE
        /// (0x20000000)    Specifies that if the object is in the config NC, the object can be moved. 
        /// 
        /// 1073741824      AR FLAG_CONFIG_ALLOW_RENAME
        /// (0x40000000)    Specifies that if the object is in the config NC, the object can be renamed.
        /// 
        /// 2147483648      DD (FLAG_DISALLOW_DELETE
        /// (0x80000000)    Specifies that the object cannot be deleted.
        /// </remarks>
        public SystemFlags SystemFlags { get; protected set; }

        /// <summary>
        /// The top-most OU where this object resides
        /// </summary>
        public string TopLevelOU {
            get {
                topLevelOU = string.Empty;
                if (!string.IsNullOrWhiteSpace(this.DN) && (this.DN.IndexOf("OU=", StringComparison.OrdinalIgnoreCase) > -1) 
                    && (this.DN.IndexOf("DC=", StringComparison.OrdinalIgnoreCase) > -1)) {
                    topLevelOU = this.DN.Substring(this.DN.LastIndexOf("OU=", StringComparison.OrdinalIgnoreCase));
                    if (this.DN.IndexOf("DC=", StringComparison.OrdinalIgnoreCase) > -1) {
                        topLevelOU = topLevelOU.Substring("OU=".Length, topLevelOU.IndexOf(",DC=", StringComparison.OrdinalIgnoreCase) - "OU=".Length).ToUpper();
                    }
                }
                return topLevelOU;
            }
        }
        private string topLevelOU;

        /// <summary>
        /// The Distinguished Name of the top-most OU where this object resides
        /// </summary>
        public string TopLevelOUDN {
            get {
                topLevelOUDN = string.Empty;
                if ((!string.IsNullOrWhiteSpace(this.DN)) && (this.DN.Contains("OU="))) {
                    topLevelOUDN = this.DN.Substring(this.DN.LastIndexOf("OU=", StringComparison.OrdinalIgnoreCase));
                }
                return topLevelOUDN;
            }
        }
        private string topLevelOUDN;

        /// <summary>
        /// All objects should have a whenChanged attribute value
        /// If this value is DateTime.MinValue, that may be an indication of insufficient permissions to read the attribute.
        /// </summary>
        public DateTime WhenChangedUTC {
            get { return whenChangedUTC; }
        }
        private DateTime whenChangedUTC;
        public DateTime WhenChanged { get { return this.WhenChangedUTC.ToLocalTime(); } }

        /// <summary>
        /// All objects should have a whenCreated attribute value
        /// If this value is DateTime.MinValue, that may be an indication of insufficient permissions to read the attribute.
        /// </summary>
        public DateTime WhenCreatedUTC {
            get { return whenCreatedUTC; }
        }
        private DateTime whenCreatedUTC;
        public DateTime WhenCreated { get { return whenCreatedUTC.ToLocalTime(); } }

        public static readonly DateTime WindowsEpochStart = DateTime.FromFileTimeUtc(0);

        #region ReplicationMetadata
        /// <summary>
        /// Contains the metadata about attribute changes
        /// </summary>
        /// <remarks>
        /// Requires elevated permissions.  Significanly increases the amount of memory and time required for queries.
        /// </remarks>
        public IReadOnlyList<ReplicationAttributeMetaData> ReplicationAttributeMetadata { get; protected set; }

        /// <summary>
        /// Contains the metadata about linked-value attribute changes
        /// </summary>
        /// <remarks>
        /// Requires elevated permissions.  Significanly increases the amount of memory and time required for queries.
        /// </remarks>
        public IReadOnlyList<ReplicationValueMetaData> ReplicationValueMetadata { get; protected set; }
        #endregion

        #region AccessRules and ObjectSecurity
        /// <summary>
        /// Returns Access Rules where the IdentityReference are SecurityIdentifier.  
        /// Use SecurityIdentifier to avoid IdentityNotMappedException when translating
        /// </summary>
        /// <remarks>
        /// Lazy-loaded due to the resources and time required.  Significanly increases the amount of memory required.
        /// </remarks>
        public AuthorizationRuleCollection AccessRules {
            get {
                if (accessRules == null) {
                    if (this.ObjectSecurity != null) {
                        accessRules = this.ObjectSecurity.GetAccessRules(includeExplicit: true, includeInherited: true, targetType: typeof(SecurityIdentifier));
                    }
                }
                return accessRules;
            }
            private set { accessRules = value; }
        }

        [NonSerialized]
        private AuthorizationRuleCollection accessRules;

        /// <summary>
        /// The Native ObjectSecurity for the object
        /// </summary>
        /// <remarks>
        /// Lazy-loaded due to the resources and time required.  Significanly increases the amount of memory required.
        /// </remarks>
        public ActiveDirectorySecurity ObjectSecurity {
            get {
                if (objectSecurity == null) {
                    objectSecurity = ADDataAccess.GetObjectSecurity(this.DN);
                }

                return objectSecurity;
            }
            private set { objectSecurity = value; }
        }

        [NonSerialized]
        private ActiveDirectorySecurity objectSecurity;
        #endregion
        #endregion

        #region Constructors
        static ADBaseObjectProperties() {
            var attributesToGet = new List<string>();
            attributesToGet.AddRange(new string[] {
                "adminCount",
                "canonicalName",
                "cn",
                "description",
                "distinguishedName",
                "dsaSignature",
                "info",
                "isCriticalSystemObject",
                "isDeleted",
                "isRecycled",
                "lastKnownParent",
                "modifyTimeStamp",
                "msDS-LastKnownRDN",
                "msDS-ReplAttributeMetaData", // heavy, requires administrator permission
                "msDS-ReplValueMetaData", // heavy, requires administrator permission
                "name",
                "objectCategory",
                "objectClass",
                "objectGuid",
                "objectSid",
                "showInAdvancedViewOnly",
                "systemFlags",
                "whenChanged",
                "whenCreated"
            });

            attributesToGet.AddRange(CustomAttributes.AttributesToGet);

            AttributesToGet = attributesToGet.Distinct().ToArray();
            attributesToGet = null;
        }

        public ADBaseObjectProperties() {
            this.Initialize();
        }

        public ADBaseObjectProperties(string dn)
            : this() {
            if (!string.IsNullOrWhiteSpace(dn)) {
                this.DN = dn;
            }
        }

        public ADBaseObjectProperties(DirectoryEntry directoryEntry, bool destroyInputObject = true)
            : this() {
            if (directoryEntry == null) {
                throw new ArgumentNullException("directoryEntry");
            }
            this.LdapDN = directoryEntry.Path;
            this.DestroyInputObject = destroyInputObject;

            foreach (string propertyName in directoryEntry.Properties.PropertyNames) {
                if (directoryEntry.Properties[propertyName] == null) continue;
                if (!this.Attributes.ContainsKey(propertyName)) {
                    object[] element = new object[directoryEntry.Properties[propertyName].Count];
                    for (int index = 0; index < directoryEntry.Properties[propertyName].Count; index++) {
                        element[index] = directoryEntry.Properties[propertyName][index];
                    }
                    this.Attributes.Add(propertyName, element);
                }
            }

            this.ProcessResults();

            if (this.DestroyInputObject) directoryEntry.Dispose();
            if (this.GetType().Name == typeof(ADBaseObjectProperties).Name) {
                this.Attributes = null;
            }
        }

        public ADBaseObjectProperties(SearchResult result, bool destroyInputObject = true)
            : this() {
            if (result == null) {
                throw new ArgumentNullException("result");
            }
            if (string.IsNullOrWhiteSpace(result.Path)) {
                throw new ArgumentNullException("result.Path");
            }
            this.LdapDN = result.Path;
            this.DestroyInputObject = destroyInputObject;

            foreach (string propertyName in result.Properties.PropertyNames) {
                if (result.Properties[propertyName] == null) continue;
                if (!this.Attributes.ContainsKey(propertyName)) {
                    object[] element = new object[result.Properties[propertyName].Count];
                    for (int index = 0; index < result.Properties[propertyName].Count; index++) element[index] = result.Properties[propertyName][index];
                    this.Attributes.Add(propertyName, element);
                }
            }

            this.ProcessResults();

            if (this.DestroyInputObject) result = null;
            if (this.GetType().Name == typeof(ADBaseObjectProperties).Name) {
                this.Attributes = null;
            }
        }

        public ADBaseObjectProperties(List<string> ldifOutput)
            : this() {
            if (ldifOutput == null) {
                throw new ArgumentNullException("ldifOutput");
            }
            var adjustedLdifOutput = ldifOutput.ReformattedLdifList();
            ldifOutput = null;

            var groupMemberships = new List<string>();
            var servicePrincipalNames = new List<string>();
            var sidHistoryList = new List<string>();

            foreach (var line in adjustedLdifOutput) {
                if (string.IsNullOrWhiteSpace(line)) continue;

                string tag = string.Empty;
                // binary values start with propertyName:: 
                if (line.IndexOf(":: ") != -1) {
                    tag = ":: ";
                }
                else if (line.IndexOf(": ") != -1) {
                    tag = ": ";
                }
                if (!string.IsNullOrWhiteSpace(tag)) {
                    string propertyName = line.Substring(0, line.IndexOf(tag));
                    string value = line.Substring(line.IndexOf(tag) + tag.Length);

                    if (propertyName == "memberOf") {
                        groupMemberships.Add(value);
                    }
                    else if (propertyName == "servicePrincipalName") {
                        servicePrincipalNames.Add(value);
                    }
                    else if (propertyName == "sidHistory") {
                        sidHistoryList.Add(value);
                    }
                    else if (!this.Attributes.ContainsKey(propertyName)) {
                        object[] element = new object[1] { value };
                        this.Attributes.Add(propertyName, element);
                    }
                }
            }

            if ((groupMemberships.Count > 0) && !this.Attributes.ContainsKey("memberOf")) {
                this.Attributes.Add("memberOf", groupMemberships.ToArray());
            }
            if ((servicePrincipalNames.Count > 0) && !this.Attributes.ContainsKey("servicePrincipalName")) {
                this.Attributes.Add("servicePrincipalName", servicePrincipalNames.ToArray());
            }
            if ((sidHistoryList.Count > 0) && !this.Attributes.ContainsKey("sidHistory")) {
                this.Attributes.Add("sidHistory", sidHistoryList.ToArray());
            }

            this.ProcessResults();

            adjustedLdifOutput = null;
            if (this.GetType().Name == typeof(ADBaseObjectProperties).Name) {
                this.Attributes = null;
            }
        }
        #endregion

        #region Methods
        public virtual object Clone() {
            var memoryStream = new MemoryStream();
            var formatter = new BinaryFormatter();
            formatter.Serialize(memoryStream, this);
            memoryStream.Position = 0;
            object clone = formatter.Deserialize(memoryStream);
            return clone;
        }

        /// <summary>
        /// Populates the ReplicationAttributeMetadata and ReplicationValueMetadata collections
        /// </summary>
        public void GetReplicationMetadata() {
            this.ReplicationAttributeMetadata = ADDataAccess.GetReplicationAttributeMetaData(this.DN);
            this.ReplicationValueMetadata = ADDataAccess.GetReplicationValueMetaData(this.DN);
        }

        public void GetObjectSecurity() {
            objectSecurity = ADDataAccess.GetObjectSecurity(this.DN);
        }

        protected virtual void Initialize() {

            this.CanonicalName = string.Empty;
            this.CN = string.Empty;
            this.Description = string.Empty;
            this.DestroyInputObject = true;
            this.DN = string.Empty;
            this.DNSDomain = string.Empty;
            this.Domain = string.Empty;
            this.Info = string.Empty;
            this.LastKnownParent = string.Empty;
            this.LdapDN = string.Empty;
            this.Name = string.Empty;
            this.ObjectCategory = string.Empty;
            this.ParentDN = string.Empty;
            this.Server = string.Empty;

            this.Attributes = new Dictionary<string, object[]>(StringComparer.OrdinalIgnoreCase);
            this.CustomAttributes = new CustomAttributes();
            this.ObjectClass = new List<string>();
            this.ReplicationAttributeMetadata = new List<ReplicationAttributeMetaData>();
            this.ReplicationValueMetadata = new List<ReplicationValueMetaData>();
        }

        protected virtual void ProcessResults() {
            try {
                #region Get DN
                if (this.Attributes.ContainsKey("distinguishedName")) {
                    this.DN = this.Attributes["distinguishedName"][0].ToString();
                }
                else {
                    this.DN = ADDataAccess.GetDNFromLdapDN(this.LdapDN);
                }

                if (!string.IsNullOrWhiteSpace(this.DN)) this.ParentDN = ADDataAccess.GetParentDN(this.DN);
                #endregion

                #region Get DSASignature
                if (this.Attributes.ContainsKey("dsaSignature")) {
                    byte[] fullSignature = (byte[])this.Attributes["dsaSignature"][0];
                    byte[] dsaGuid = null;
                    if (fullSignature.Length > 16) {
                        fullSignature.Skip(fullSignature.Length - 16).ToArray();
                    }
                    else {
                        dsaGuid = fullSignature;
                    }
                    if ((dsaGuid != null) && (dsaGuid.Length == 16)) {
                        this.DSASignature = new Guid(dsaGuid);
                    }
                }
                #endregion

                if (this.Attributes.ContainsKey("adminCount")) this.AdminCount = Convert.ToInt32(this.Attributes["adminCount"][0]);
                if (this.Attributes.ContainsKey("canonicalName")) this.CanonicalName = this.Attributes["canonicalName"][0].ToString();
                if (this.Attributes.ContainsKey("cn")) this.CN = this.Attributes["cn"][0].ToString();
                if (this.Attributes.ContainsKey("description")) this.Description = this.Attributes["description"][0].ToString();
                if (this.Attributes.ContainsKey("info")) this.Info = this.Attributes["info"][0].ToString().Trim();
                if (this.Attributes.ContainsKey("isCriticalSystemObject")) this.IsCriticalSystemObject = Convert.ToBoolean(this.Attributes["isCriticalSystemObject"][0]);
                if (this.Attributes.ContainsKey("isDeleted")) this.IsDeleted = Convert.ToBoolean(this.Attributes["isDeleted"][0]);
                if (this.Attributes.ContainsKey("isRecycled")) this.IsRecycled = Convert.ToBoolean(this.Attributes["isRecycled"][0]);
                if (this.Attributes.ContainsKey("lastKnownParent")) this.LastKnownParent = this.Attributes["lastKnownParent"][0].ToString();
                if (this.Attributes.ContainsKey("modifyTimeStamp")) {
                    DateTime modifyTimeStampUTC = DateTime.MinValue;
                    if (DateTime.TryParse(this.Attributes["modifyTimeStamp"][0].ToString(), out modifyTimeStampUTC)) {
                        ModifyTimeStampUTC = modifyTimeStampUTC;
                    }
                }
                if (this.Attributes.ContainsKey("name")) this.Name = this.Attributes["name"][0].ToString();
                if (this.Attributes.ContainsKey("objectCategory")) this.ObjectCategory = this.Attributes["objectCategory"][0].ToString();
                this.ObjectClass = ADDataAccess.GetMultiValuedAttribute<string>("objectClass", this.Attributes);
                if (this.Attributes.ContainsKey("objectGUID")) {
                    if (this.Attributes["objectGUID"][0] as byte[] != null) {
                        this.ObjectGuid = new Guid((byte[])this.Attributes["objectGUID"][0]);
                    }
                    else {
                        this.ObjectGuid = new Guid(this.Attributes["objectGUID"][0].ToString().FromBase64());
                    }
                }
                if (this.Attributes.ContainsKey("objectSid")) {
                    if (this.Attributes["objectSid"][0] as byte[] != null) {
                        this.ObjectSid = new SecurityIdentifier(this.Attributes["objectSid"][0] as byte[], 0);
                    }
                    else {
                        this.ObjectSid = new SecurityIdentifier(this.Attributes["objectSid"][0].ToString().FromBase64(), 0);
                    }
                }

                if (this.Attributes.ContainsKey("msDS-LastKnownRDN")) this.MSDSLastKnownRDN = this.Attributes["msDS-LastKnownRDN"][0].ToString();

                if (this.Attributes.ContainsKey("showInAdvancedViewOnly")) this.ShowInAdvancedViewOnly = Convert.ToBoolean(this.Attributes["showInAdvancedViewOnly"][0]);
                if (this.Attributes.ContainsKey("systemFlags")) this.SystemFlags = (SystemFlags)Convert.ToInt32(this.Attributes["systemFlags"][0]);

                #region whenChanged attribute
                if (this.Attributes.ContainsKey("whenChanged")) {
                    if (!DateTime.TryParse(this.Attributes["whenChanged"][0].ToString(), out whenChangedUTC)) {
                        Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} warning: unable to parse datetime for whenChanged: {3}",
                            DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId,
                            CustomExtensions.CurrentMethodName(), this.Attributes["whenChanged"][0].ToString()));
                    }
                }
                #endregion

                #region whenCreated attribute
                if (this.Attributes.ContainsKey("whenCreated")) {
                    if (!DateTime.TryParse(this.Attributes["whenCreated"][0].ToString(), out whenCreatedUTC)) {
                        Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} warning: unable to parse datetime for whenCreated: {3}",
                            DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId,
                            CustomExtensions.CurrentMethodName(), this.Attributes["whenCreated"][0].ToString()));
                    }
                }
                #endregion

                #region Normal attribute metadata
                // msDS-ReplAttributeMetaData
                if (this.Attributes.ContainsKey("msDS-ReplAttributeMetaData") && this.Attributes["msDS-ReplAttributeMetaData"].Length > 0) {
                    var replicationAttributeMetaData = new List<ReplicationAttributeMetaData>();
                    var xmlSerializer = new XmlSerializer(typeof(ReplicationAttributeMetaData));
                    for (int index = 0; index < this.Attributes["msDS-ReplAttributeMetaData"].Length; index++) {
                        string rawAttributeValue = this.Attributes["msDS-ReplAttributeMetaData"][index].ToString();
                        if (!string.IsNullOrWhiteSpace(rawAttributeValue)) {
                            var parsedAttributeValue = string.Empty;
                            try {
                                parsedAttributeValue = Regex.Replace(rawAttributeValue, "&(?!(amp|apos|quot|lt|gt);)", "&amp;");
                                using (var stringReader = new StringReader(parsedAttributeValue)) {
                                    var replValueMetaData = (ReplicationAttributeMetaData)xmlSerializer.Deserialize(stringReader);
                                    if (replValueMetaData != null) {
                                        replicationAttributeMetaData.Add(replValueMetaData);
                                    }
                                }
                            }
                            catch (Exception e) {
                                Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} Error deserializing msDS-ReplAttributeMetaData: {3}",
                                    DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(), e.VerboseExceptionString()));
                                Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} msDS-ReplAttributeMetaData raw value: {3}",
                                    DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(), rawAttributeValue));
                                Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} msDS-ReplAttributeMetaData parsed value: {3}",
                                    DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(), parsedAttributeValue));
                            }
                        }
                    }

                    this.ReplicationAttributeMetadata = new List<ReplicationAttributeMetaData>(replicationAttributeMetaData);
                }
                #endregion

                #region Linked-value attribute metadata
                if (this.Attributes.ContainsKey("msDS-ReplValueMetaData") && this.Attributes["msDS-ReplValueMetaData"].Length > 0) {
                    var replicationValueMetaData = new List<ReplicationValueMetaData>();
                    var xmlSerializer = new XmlSerializer(typeof(ReplicationValueMetaData));
                    for (int index = 0; index < this.Attributes["msDS-ReplValueMetaData"].Length; index++) {
                        string rawAttributeValue = this.Attributes["msDS-ReplValueMetaData"][index].ToString();
                        if (!string.IsNullOrWhiteSpace(rawAttributeValue)) {
                            var parsedAttributeValue = string.Empty;
                            try {
                                parsedAttributeValue = Regex.Replace(rawAttributeValue, "&(?!(amp|apos|quot|lt|gt);)", "&amp;");
                                using (var stringReader = new StringReader(parsedAttributeValue)) {
                                    var replValueMetaData = (ReplicationValueMetaData)xmlSerializer.Deserialize(stringReader);
                                    if (replValueMetaData != null) {
                                        replicationValueMetaData.Add(replValueMetaData);
                                    }
                                }
                            }
                            catch (Exception e) {
                                Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} Error deserializing msDS-ReplValueMetaData: {3}",
                                    DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(), e.VerboseExceptionString()));
                                Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} msDS-ReplValueMetaData raw value: {3}",
                                    DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(), rawAttributeValue));
                                Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} msDS-ReplValueMetaData parsed value: {3}",
                                    DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(), parsedAttributeValue));
                            }
                        }
                    }

                    this.ReplicationValueMetadata = new List<ReplicationValueMetaData>(replicationValueMetaData);
                }
                #endregion

                if (!string.IsNullOrWhiteSpace(this.LdapDN)) {
                    this.Server = ADDataAccess.GetServerFromLdapDN(this.LdapDN);
                }
                if (this.Server == null) {
                    this.Server = string.Empty;
                }

                this.CustomAttributes.ProcessResults(this.Attributes);

                this.IsValid = !string.IsNullOrWhiteSpace(this.DN);
            }
            catch (Exception e) {
                Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} Error: {3}", DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(), e.VerboseExceptionString()));
                throw;
            }

            this.Domain = ADDataAccess.GetDomainNetBiosNameFromDNOrDNSName(this.DN) ?? string.Empty;

            if (!string.IsNullOrWhiteSpace(this.Domain)) {
                this.Domain = this.Domain.ToUpper();
            }

            if ((ADDataAccess.ADDomainInfo != null) && (ADDataAccess.ADDomainInfo.Count > 0)) {
                var domainInfo = ADDataAccess.ADDomainInfo.Where(x => x.NetbiosName == this.Domain).FirstOrDefault();
                if (domainInfo != null) {
                    this.DNSDomain = domainInfo.DNSName;
                }
            }

            if (string.IsNullOrWhiteSpace(this.DNSDomain)) {
                this.DNSDomain = ADDataAccess.GetDomainDnsName(this.DN);
            }
        }

        public T Refresh<T>() where T : ADBaseObjectProperties, new() {
            string filter = "distinguishedName=" + this.DN;
            var search = ADDataAccess.GetADObjectProperties<T>(
                this.DN, null, filter, PartitionScope.LocalPartition);
            if (search.Count == 1) {
                return search.First() as T;
            }

            return null;
        }

        /// <summary>
        /// Helper method to trim string properties of this object 
        /// </summary>
        public void TrimStringProperties() {
            PropertyInfo[] properties = this.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static);

            foreach (PropertyInfo propertyInfo in properties) {
                // Only work with strings
                if (propertyInfo.PropertyType != typeof(string)) { continue; }

                // If not writable then cannot null it; if not readable then cannot check it's value
                if (!propertyInfo.CanWrite || !propertyInfo.CanRead) { continue; }

                MethodInfo getMethod = propertyInfo.GetGetMethod(nonPublic: false);
                MethodInfo setMethod = propertyInfo.GetSetMethod(nonPublic: false);

                // Get and set methods have to be public
                if (getMethod == null) { continue; }
                if (setMethod == null) { continue; }

                string currentValue = propertyInfo.GetValue(this, new string[] { }) as string;
                if (currentValue != null) {
                    propertyInfo.SetValue(this, currentValue.Trim(), new object[] { });
                }
            }
        }

        #region Overrides
        [DebuggerStepThroughAttribute]
        public override int GetHashCode() {
            unchecked {
                int hash = 13;

                return (hash * 7) + this.ObjectGuid.GetHashCode();
            }
        }

        [DebuggerStepThroughAttribute]
        public override bool Equals(object obj) {
            var baseObject = obj as ADBaseObjectProperties;
            if (obj == null) return false;

            if ((this.ObjectGuid != Guid.Empty) && (baseObject.ObjectGuid != Guid.Empty)) {
                return this.ObjectGuid == baseObject.ObjectGuid;
            }

            if (!string.IsNullOrWhiteSpace(this.DN) && !string.IsNullOrWhiteSpace(baseObject.DN)) {
                return this.DN == baseObject.DN;
            }

            return this.Name == baseObject.Name;
        }

        [DebuggerStepThroughAttribute]
        public override string ToString() {
            string details = this.DN;
            if (string.IsNullOrWhiteSpace(details)) details = "N/A";
            return details;
        }
        #endregion

        #region IComparable<T> implementation
        [DebuggerStepThroughAttribute]
        public int CompareTo(ADBaseObjectProperties adBaseObject) {
            return this.ObjectGuid.CompareTo(adBaseObject.ObjectGuid);
        }
        #endregion

        #region IEnumerable<T> implementation
        [DebuggerStepThroughAttribute]
        public System.Collections.Generic.IEnumerator<ADBaseObjectProperties> GetEnumerator() {
            yield return this;
        }

        [DebuggerStepThroughAttribute]
        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() {
            return this.GetEnumerator();
        }
        #endregion
        #endregion

    }
}
